﻿module Lines

open HtmlDom
open StyledElements
open TextRendering
open Boxes
open Floats
open ComputedStyle


type Layers = Layers of PlacedLayout list

(* line breaking algorithm 
    breakes a sequence of 'units' (words or inline-boxes) into a stack of lines, 
    bases on available withs, taking floats into consideration
*)

(* callback to calculate dimensions of an inline block *)
type InlineElemLayouter = IStyledElement -> int * int option -> Layers -> Layout*Layers

(* units composing an line flow - before layout *)
type InlineUnit = 
        | WordUnit of string * IStyledText
        | InlineBlockUnit of IStyledElement
        | Float of IStyledElement*bool

(* An inline unit (word or inline block) where we know the dimensions but not yet the position. *)
type LayoutedInlineUnit =
    | LayoutedWordUnit of LayoutedTextBox
    | LayoutedInlineBlock of Layout with
    member this.width =
        match this with
        | LayoutedInlineBlock(layout) -> layout.size.width
        | LayoutedWordUnit(layoutedTextBox) -> layoutedTextBox.width
    member this.widthIncludingTrailingWhitespace =
        match this with
        | LayoutedInlineBlock(layout) -> layout.size.width
        | LayoutedWordUnit(layoutedTextBox) -> layoutedTextBox.widthIncludingTrailingWhitespace
    member this.height =
        match this with
        | LayoutedInlineBlock(layout) -> layout.size.height
        | LayoutedWordUnit(layoutedTextBox) -> layoutedTextBox.height


let formatUnit text (node:IStyledText) =
    let formattedText = computeText text node 
    let breakAfter = text.EndsWith("\n")
    LayoutedWordUnit(LayoutedTextBox(node, formattedText, breakAfter))


(*  A line as a sequence of size units, and the "frame" in which they can be rendered.    
    Frame is min left indent, min right indent and y offset - relative to outer box
    We dont know the actual width yet, so we can't precisely place units with alignement left/right/justified 
    *)
type Line = Line of LayoutedInlineUnit list * int * int * int
    with
    member this.height = 
        let (Line(units, l, r,y)) = this
        List.max [for u in units -> u.height]
        

(* availableWidth (after indents substracted), leftIndent, rightIndent for a line.
    Recalcualted when a float is encountered 
  *)
type LineFrame = LineFrame of int * int * int with
    member this.availableWidth =
        let (LineFrame(availWidth,l,r)) = this
        availWidth
    member this.left =
        let (LineFrame(availWidth,l,r)) = this
        l
    member this.right =
        let (LineFrame(availWidth,l,r)) = this
        r

 
let calcLineFrame (availWidth:int) (flow:Flow) (flowRel:FloatOffset) (y:int) =
    let (FloatOffset(l, r, yOffset)) = flowRel
    let flowRel1 = FloatOffset(l, r, yOffset + y)
    let (l, r) = flow.indents flowRel1
    LineFrame(availWidth - l - r, l, r)


type LineBoxesBuilder = { 
        y : int
        lines : Line list
        buffer : LayoutedInlineUnit list
        // floats which cannot be floated on the current line
        floatBuffer : (Layout*bool) list
        // current width of buffer
        width : int 
        // available with for line boxes (not taking floats into consideration)
        availableWidth : int
        flow:Flow
        flowRel:FloatOffset
        lineFrame : LineFrame
        layers : Layers
} with
    member this.appendLine line =
        let lines1 = this.lines @ [line]
        let y1 = this.y + line.height
        {this with lines = lines1; y=y1 }

    member this.newLine  =
        let builder1 = this.closeLineBuffer
        let lineFrame = calcLineFrame this.availableWidth this.flow this.flowRel this.y
        {builder1 with lineFrame = lineFrame }

    member this.closeLineBuffer =
        match this.buffer with
        | [] -> this // already closed
        | lst ->
            let (LineFrame(w,l,r)) = this.lineFrame
            let line = Line(this.buffer,l,r,this.y)
            let builder1 = this.appendLine line
            let builder2 = builder1.unbufferFloats
            let lineFrame1 = calcLineFrame builder2.availableWidth builder2.flow builder2.flowRel builder2.y
            {builder2 with buffer = []; width=0; lineFrame=lineFrame1 }
 
    member this.appendToLineBuffer unit unitWidth =
        let width1 = this.width + unitWidth
        let buffer1 = this.buffer @ [unit]
        {this with width=width1; buffer=buffer1}

    member this.appendToLineBufferAndCheckBreak unit unitWidth =
        let builder = this.appendToLineBuffer unit unitWidth
        match unit with 
        | LayoutedWordUnit(LayoutedTextBox(text, formattedText, true)) ->
            builder.closeLineBuffer
        | _ -> builder

    member this.appendInline (unit:LayoutedInlineUnit) =
        let builder =
            if (this.width + unit.width) > this.lineFrame.availableWidth
            then 
                this.closeLineBuffer
            else this
        builder.appendToLineBufferAndCheckBreak unit unit.widthIncludingTrailingWhitespace            

    member this.appendFloat (layout:Layout) isLeft =
        let (Flow(lefts, rights)) = this.flow
        if this.buffer.Length>0 && this.width + layout.size.width > this.lineFrame.availableWidth
        then
            this.bufferFloat layout isLeft
        else
            this.placeFloat layout isLeft

    member this.bufferFloat (layout:Layout) isLeft =
        // not room on this line, float on next line
        {this with floatBuffer=((layout,isLeft)::this.floatBuffer)}        

    member this.unbufferFloats =
        let this2 = this.floatBuffer |> List.fold (fun (t:LineBoxesBuilder) (layout,isLeft) -> t.placeFloat layout isLeft) this
        {this2 with floatBuffer=[]}

    member this.placeFloat (layout:Layout) isLeft =
        let y = this.y + this.flowRel.y
        let flow1 = this.flow.add isLeft layout y (this.flowRel.left, this.flowRel.right) this.availableWidth
        (* recalcualte line frame *)
        let lineFrame1 = calcLineFrame this.availableWidth flow1 this.flowRel this.y
        {this with flow=flow1; lineFrame=lineFrame1}

let initLineBuilder availableWidth flow flowRel layers = 
    {
        y = 0
        availableWidth=availableWidth
        buffer=[]
        lines=[]
        width=0
        flow=flow
        flowRel=flowRel
        floatBuffer=[]
        lineFrame = calcLineFrame availableWidth flow flowRel 0
        layers = layers
        }
  
(*
    Partitions a flow of inline units into lines
*)
let toLines (layoutInlineElem:InlineElemLayouter) (inlineUnits:InlineUnit list) (availableWidth:int,parentheight:int option) 
                                                            (flow:Flow) (flowRel:FloatOffset) (layers:Layers) : Line list *Flow*Layers =

    let consumeUnit (builder:LineBoxesBuilder) (unit:InlineUnit) =
        match unit with 
        | InlineBlockUnit(elem) ->
            let layout, layers = layoutInlineElem elem (availableWidth,None) builder.layers
            builder.appendInline (LayoutedInlineBlock(layout))
        | Float(elem, isLeft) ->
            let layout, layers = layoutInlineElem elem (availableWidth,None) builder.layers
            builder.appendFloat layout isLeft
        | WordUnit(text,node) ->
            let formatted = formatUnit text node
            builder.appendInline formatted
 
    let builder = initLineBuilder availableWidth flow flowRel layers
    let builder = inlineUnits |> List.fold consumeUnit builder
    let builder = builder.closeLineBuffer
    builder.lines, builder.flow, layers
